﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml.Linq;
using System.IO;
using System.Diagnostics;

namespace DbmlUpdater
{
	public class Worker
	{
		private string sqlMetalPath;
		private Action<string> outputWriter = null;
		private Action<string> statusBarText = null;

		#region names and namespaces

		private static XNamespace xmlnsDbml = "http://schemas.microsoft.com/linqtosql/dbml/2007";
		private static XNamespace xmlnsConfig = "http://vipper.peshiba.cz/schemas/DbmlUpdater/v2";

		private static XName xnameDbml = XName.Get("DBML", xmlnsConfig.NamespaceName);
		private static XName xnameTables = XName.Get("Tables", xmlnsConfig.NamespaceName);
		private static XName xnameTable = XName.Get("Table", xmlnsConfig.NamespaceName);
		private static XName xnameViews = XName.Get("Views", xmlnsConfig.NamespaceName);
		private static XName xnameView = XName.Get("View", xmlnsConfig.NamespaceName);
		private static XName xnameFunctions = XName.Get("Functions", xmlnsConfig.NamespaceName);
		private static XName xnameFunction = XName.Get("Function", xmlnsConfig.NamespaceName);
		private static XName xnameSProcs = XName.Get("SProcs", xmlnsConfig.NamespaceName);
		private static XName xnameSProc = XName.Get("SProc", xmlnsConfig.NamespaceName);
		private static XName xnameCode = XName.Get("Code", xmlnsConfig.NamespaceName);

		private static XName xnameDbmlTable = XName.Get("Table", xmlnsDbml.NamespaceName);
		private static XName xnameDbmlFunction = XName.Get("Function", xmlnsDbml.NamespaceName);
		private static XName xnameDbmlAssociation = XName.Get("Association", xmlnsDbml.NamespaceName);
		private static XName xnameDbmlType = XName.Get("Type", xmlnsDbml.NamespaceName);
		//private static XName xname = XName.Get("", xmlnsConfig.NamespaceName);
		//private static XName xname = XName.Get("", xmlnsConfig.NamespaceName);
		//private static XName xname = XName.Get("", xmlnsConfig.NamespaceName);

		#endregion

		#region

		private WorkerItemMode tablesMode = WorkerItemMode.None;
		private WorkerItemMode viewsMode = WorkerItemMode.None;
		private WorkerItemMode functionsMode = WorkerItemMode.None;
		private WorkerItemMode sprocsMode = WorkerItemMode.None;

		private XDocument config;
		private XElement configDbml;
		private XElement configCode;

		private XElement tables;
		private XElement views;
		private XElement functions;
		private XElement sprocs;

		#endregion

		public Worker(Action<string> outputWriter, Action<string> statusBarText)
		{
			this.outputWriter = outputWriter;
			this.statusBarText = statusBarText;
			if (this.statusBarText == null)
			{
				this.statusBarText = s => { };
			}

			WriteLine();
			WriteLine(" DBML Updater v2.1");
			WriteLine(" ------------------------------------------------------------------------------ ");


			//outputWriter("\n");
			//outputWriter(" ------------------------------------------------------------------------------ \n");
			//outputWriter("                                                           " + DateTime.Now.ToString("dd.MM.yyyy HH:mm:ss") + "\n");
		}

		public void Generate(string fileName, out string codeFileName)
		{
			codeFileName = null;

			string dbmlFileName = Path.Combine(Path.GetDirectoryName(fileName), Path.GetFileNameWithoutExtension(fileName)) + ".tmp";

			config = XDocument.Load(fileName);

			if (!config.Root.Name.LocalName.Equals("DbmlUpdater", StringComparison.CurrentCultureIgnoreCase))
				return;

			WriteLine(" " + Path.GetFileName(fileName));

			configDbml = config.Root.Element(xnameDbml);

			bool generateDBML = true;

			if (configDbml != null)
				if (configDbml.Attribute("generate") != null)
					generateDBML = bool.Parse(configDbml.Attribute("generate").Value);


			Write("  - Generating DBML file... ");
			statusBarText("Generating DBML file...");

			tables = configDbml.Element(xnameTables);
			tablesMode = WorkerItemMode.AllExceptListed;
			if (tables != null)
			{
				var tablesModeAttr = tables.Attribute("mode");
				if (tablesModeAttr != null)
				{
					if (!Enum.TryParse<WorkerItemMode>(tablesModeAttr.Value, out tablesMode))
						tablesMode = WorkerItemMode.AllExceptListed;
				}
			}
			views = configDbml.Element(xnameViews);
			viewsMode = WorkerItemMode.None;
			if (views != null)
			{
				var viewsModeAttr = views.Attribute("mode");
				if (viewsModeAttr != null)
				{
					if (!Enum.TryParse<WorkerItemMode>(viewsModeAttr.Value, out viewsMode))
						viewsMode = WorkerItemMode.None;
				}
			}
			functions = configDbml.Element(xnameFunctions);
			functionsMode = WorkerItemMode.None;
			if (functions != null)
			{
				var functionsModeAttr = functions.Attribute("mode");
				if (functionsModeAttr != null)
				{
					if (!Enum.TryParse<WorkerItemMode>(functionsModeAttr.Value, out functionsMode))
						functionsMode = WorkerItemMode.None;
				}
			}
			sprocs = configDbml.Element(xnameSProcs);
			sprocsMode = WorkerItemMode.None;
			if (sprocs != null)
			{
				var sprocsModeAttr = sprocs.Attribute("mode");
				if (sprocsModeAttr != null)
				{
					if (!Enum.TryParse<WorkerItemMode>(sprocsModeAttr.Value, out sprocsMode))
						sprocsMode = WorkerItemMode.None;
				}
			}

			if (generateDBML)
			{
				if (File.Exists(dbmlFileName))
					File.Delete(dbmlFileName);

				string connectionString = configDbml.Attribute("connectionString").Value;

				if (!GenerateDBML(dbmlFileName, connectionString, viewsMode != WorkerItemMode.None, functionsMode != WorkerItemMode.None, sprocsMode != WorkerItemMode.None))
					return;
			}
			else
			{
				WriteLine("Disabled");
				statusBarText("Generating DBML file... Disabled");
			}

			Write("  - Checking DBML file exists... ");
			statusBarText("Checking DBML file exists...");
			if (!File.Exists(dbmlFileName))
			{
				WriteLine("Not found");
				statusBarText("Checking DBML file exists... Not found");
				return;
			}
			WriteLine("OK");
			statusBarText("Checking DBML file exists... OK");

			if (!ApplyChanges(configDbml, dbmlFileName))
				return;

			configCode = config.Root.Element(xnameCode);
			bool generateCode = true;

			if (configCode != null)
				if (configCode.Attribute("generate") != null)
					generateCode = bool.Parse(configCode.Attribute("generate").Value);

			Write("  - Generating Code file... ");
			statusBarText("Generating Code file...");
			if (generateCode)
			{
				string lang = configCode.Attribute("lang") != null ? configCode.Attribute("lang").Value.ToLower() : "cs";
				string ns = configCode.Attribute("namespace") != null ? configCode.Attribute("namespace").Value : "";
				string context = configCode.Attribute("context") != null ? configCode.Attribute("context").Value : "";
				codeFileName = Path.Combine(Path.GetDirectoryName(dbmlFileName), Path.GetFileNameWithoutExtension(dbmlFileName)) + "." + lang;

				if (File.Exists(codeFileName))
					File.Delete(codeFileName);

				if (!GenerateCode(dbmlFileName, codeFileName, ns, context))
					return;
			}
			else
			{
				WriteLine("Disabled");
				statusBarText("Generating Code file... Disabled");
			}

			Write("  - Deleting DBML file... ");
			statusBarText("Deleting DBML file...");
			bool deleteOnComplete = configDbml.Attribute("deleteOnComplete") != null ? bool.Parse(configDbml.Attribute("deleteOnComplete").Value) : generateDBML;
			if (deleteOnComplete)
			{
				File.Delete(dbmlFileName);
				WriteLine("OK");
				statusBarText("Deleting DBML file... OK");
			}
			else
			{
				WriteLine("Disabled");
				statusBarText("Deleting DBML file... Disabled");
			}
		}

		private bool ApplyChanges(XElement dbmlOptions, string dbmlFileName)
		{
			WriteLine("  - Modifing DBML file");
			statusBarText("Modifing DBML file");

			XDocument dbml = XDocument.Load(dbmlFileName);
			XDocument tablesDbml = viewsMode != WorkerItemMode.None ? XDocument.Load(dbmlFileName + ".tables") : XDocument.Parse("<?xml version=\"1.0\" encoding=\"utf-8\"?><Database Name=\"DbmlUpdaterTest\" xmlns=\"http://schemas.microsoft.com/linqtosql/dbml/2007\" />");

			if (File.Exists(dbmlFileName + ".tables"))
				File.Delete(dbmlFileName + ".tables");

			IEnumerable<string> tableNames = tablesDbml.Root.Elements(xnameDbmlTable).Select(p => p.Attribute("Name").Value).Distinct();
			foreach (var el in dbml.Root.Elements(xnameDbmlTable))
			{
				if (!tableNames.Contains(el.Attribute("Name").Value, StringComparer.CurrentCultureIgnoreCase))
				{
					el.Add(new XAttribute("isView", true));
				}
			}


			#region

			WriteLine("    - Tables");
			statusBarText("Modifing DBML file - Tables");

			if (tablesMode == WorkerItemMode.None)
			{
				dbml.Root.Elements(xnameDbmlTable).Where(p => p.Attribute("isView") == null).ToList().ForEach(p => p.Remove());
			}
			else
			{
				List<string> listedTables = new List<string>();
				if (tables != null)
					listedTables.AddRange(tables.Elements(xnameTable).Select(p => p.Value).Distinct());

				foreach (var t in dbml.Root.Elements(xnameDbmlTable).Where(p => p.Attribute("isView") == null).ToList())
				{
					bool isListed = listedTables.Contains(t.Attribute("Name").Value, StringComparer.CurrentCultureIgnoreCase);

					if ((isListed && tablesMode == WorkerItemMode.AllExceptListed) || (!isListed && tablesMode == WorkerItemMode.OnlyListed))
					{
						var xxx = t.Descendants(xnameDbmlAssociation).Select(p => p.Attribute("Name").Value).ToList();
						foreach (var a in xxx)
						{
							var zzz = dbml.Descendants(xnameDbmlAssociation).Where(p => p.Attribute("Name") != null && a.Equals(p.Attribute("Name").Value, StringComparison.CurrentCultureIgnoreCase)).ToList();
							foreach (var b in zzz)
								b.Remove();
						}

						t.Remove();
					}
				}
			}

			WriteLine("    - Views");
			statusBarText("Modifing DBML file - Views");

			if (viewsMode != WorkerItemMode.None)
			{
				List<string> listedViews = new List<string>();
				if (views != null)
					listedViews.AddRange(views.Elements(xnameView).Select(p => p.Value).Distinct());

				foreach (var t in dbml.Root.Elements(xnameDbmlTable).Where(p => p.Attribute("isView") != null).ToList())
				{
					bool isListed = listedViews.Contains(t.Attribute("Name").Value, StringComparer.CurrentCultureIgnoreCase);

					if ((isListed && viewsMode == WorkerItemMode.AllExceptListed) || (!isListed && viewsMode == WorkerItemMode.OnlyListed))
						t.Remove();
				}
			}
			// removes all isView attributes from dbml
			dbml.Root.Elements(xnameDbmlTable).Select(p => p.Attribute("isView")).ToList().ForEach(p => { if (p != null) { p.Remove(); } });

			WriteLine("    - Functions");
			statusBarText("Modifing DBML file - Functions");

			if (functionsMode != WorkerItemMode.None)
			{
				List<string> listedFunctions = new List<string>();
				if (functions != null)
					listedFunctions.AddRange(functions.Elements(xnameFunction).Select(p => p.Value).Distinct());

				foreach (var t in dbml.Root.Elements(xnameDbmlFunction).Where(p => p.Attribute("IsComposable") == null ? false : bool.Parse(p.Attribute("IsComposable").Value) == true).ToList())
				{
					bool isListed = listedFunctions.Contains(t.Attribute("Name").Value, StringComparer.CurrentCultureIgnoreCase);

					if ((isListed && functionsMode == WorkerItemMode.AllExceptListed) || (!isListed && functionsMode == WorkerItemMode.OnlyListed))
						t.Remove();
				}
			}

			WriteLine("    - Storage procedures");
			statusBarText("Modifing DBML file - Storage procedures");

			if (sprocsMode != WorkerItemMode.None)
			{
				List<string> listedSProcs = new List<string>();
				if (sprocs != null)
					listedSProcs.AddRange(sprocs.Elements(xnameSProc).Select(p => p.Value).Distinct());

				foreach (var t in dbml.Root.Elements(xnameDbmlFunction).Where(p => p.Attribute("IsComposable") == null ? true : bool.Parse(p.Attribute("IsComposable").Value) == false).ToList())
				{
					bool isListed = listedSProcs.Contains(t.Attribute("Name").Value, StringComparer.CurrentCultureIgnoreCase);

					if ((isListed && sprocsMode == WorkerItemMode.AllExceptListed) || (!isListed && sprocsMode == WorkerItemMode.OnlyListed))
						t.Remove();
				}
			}

			#endregion

			#region Removing prefixes

			Write("      - Removing prefixes... ");
			statusBarText("Modifing DBML file - Removing prefixes...");

			string removePrefix = dbmlOptions.Attribute("removePrefix") != null ? dbmlOptions.Attribute("removePrefix").Value : string.Empty;
			if (string.IsNullOrEmpty(removePrefix))
			{
				WriteLine("Skipped");
				statusBarText("Modifing DBML file - Removing prefixes... Skipped");
			}
			else
			{
				var associations = dbml.Root.Descendants(xnameDbmlAssociation);
				var associationsTypes = associations.Select(p => p.Attribute(XName.Get("Type")));
				var associationsMembers = associations.Select(p => p.Attribute(XName.Get("Member")));

				foreach (var t in associationsTypes)
				{
					if (t.Value.StartsWith(removePrefix))
						t.Value = t.Value.Substring(removePrefix.Length);
				}

				foreach (var m in associationsMembers)
				{
					if (m.Value.StartsWith(removePrefix))
						m.Value = m.Value.Substring(removePrefix.Length);
				}

				foreach (var el in dbml.Root.Elements())
				{
					if (el.Name.LocalName.Equals("Table", StringComparison.CurrentCultureIgnoreCase))
					{
						XAttribute member = el.Attribute("Member");
						if (member == null)
							continue;

						if (!member.Value.StartsWith(removePrefix))
							continue;

						string oldName = member.Value;
						string newName = member.Value.Substring(removePrefix.Length);

						member.Value = newName;

						XElement type = el.Element(xnameDbmlType);
						if (type != null)
						{
							XAttribute typeName = type.Attribute(XName.Get("Name"));
							if (typeName != null)
							{
								typeName.Value = typeName.Value.Substring(removePrefix.Length);
							}
						}
					}
				}

				WriteLine("OK");
				statusBarText("Modifing DBML file - Removing prefixes... OK");
			}

			#endregion


			dbml.Save(dbmlFileName);

			return true;
		}

		private bool GenerateDBML(string dbmlFileName, string connectionString, bool views, bool functions, bool sprocs)
		{
			List<string> args = new List<string>();
			args.Add("/conn:\"" + connectionString + "\"");
			if (views)
				args.Add("/views");
			if (functions)
				args.Add("/functions");
			if (sprocs)
				args.Add("/sprocs");

			args.Add("/pluralize");

			args.Add("/dbml:\"" + dbmlFileName + "\"");


			ProcessStartInfo pi = new ProcessStartInfo(sqlMetalPath, string.Join(" ", args.ToArray()));
			pi.CreateNoWindow = true;
			pi.UseShellExecute = false;
			pi.RedirectStandardOutput = true;
			pi.StandardOutputEncoding = Encoding.UTF8;
			Process p = Process.Start(pi);
			p.WaitForExit();

			if (p.ExitCode == 0 && views)
			{
				args = new List<string>();
				args.Add("/conn:\"" + connectionString + "\"");
				args.Add("/pluralize");

				args.Add("/dbml:\"" + dbmlFileName + ".tables\"");

				pi = new ProcessStartInfo(sqlMetalPath, string.Join(" ", args.ToArray()));
				pi.CreateNoWindow = true;
				pi.UseShellExecute = false;
				pi.RedirectStandardOutput = true;
				pi.StandardOutputEncoding = Encoding.UTF8;
				p = Process.Start(pi);
				p.WaitForExit();
			}

			if (p.ExitCode == 0)
			{
				WriteLine("OK");
				statusBarText("Generating DBML file... OK");
				return true;
			}

			string error = p.StandardOutput.ReadToEnd().Split('\n').FirstOrDefault(e => e.StartsWith("ERROR", StringComparison.CurrentCultureIgnoreCase));

			WriteLine("ERROR: " + error);
			statusBarText("Generating DBML file... " + error);

			System.Threading.Thread.Sleep(2000);

			return false;
		}

		private bool GenerateCode(string dbmlFileName, string codeFileName, string ns, string context)
		{
			List<string> args = new List<string>();
			args.Add("/code:\"" + codeFileName + "\"");

			if (!string.IsNullOrEmpty(ns))
				args.Add("/namespace:" + ns);

			if (!string.IsNullOrEmpty(context))
				args.Add("/context:" + context + "Context");

			args.Add("\"" + dbmlFileName + "\"");


			ProcessStartInfo pi = new ProcessStartInfo(sqlMetalPath, string.Join(" ", args.ToArray()));
			pi.CreateNoWindow = true;
			pi.UseShellExecute = false;
			pi.RedirectStandardOutput = true;
			Process p = Process.Start(pi);
			p.WaitForExit();

			if (p.ExitCode == 0)
			{
				WriteLine("OK");
				statusBarText("Generating Code file... OK");
				return true;
			}

			string error = p.StandardOutput.ReadToEnd().Split('\n').FirstOrDefault(e => e.StartsWith("ERROR", StringComparison.CurrentCultureIgnoreCase));

			WriteLine("ERROR: " + error);
			statusBarText("Generating Code file... " + error);

			System.Threading.Thread.Sleep(1000);

			return false;
		}

		#region write

		private void Write(string s)
		{
			if (outputWriter == null)
				Console.Write(s);
			else
				outputWriter(s);
		}

		private void Write(string s, params object[] args)
		{
			Write(string.Format(s, args));
		}

		private void WriteLine()
		{
			Write("\n");
		}

		private void WriteLine(string s)
		{
			Write(s);
			WriteLine();
		}

		private void WriteLine(string s, params object[] args)
		{
			WriteLine(string.Format(s, args));
		}

		#endregion

		public bool CheckWinSDK()
		{
			//sqlMetalPath = "";


			string[] programFilesFolders = new string[] 
			{ 
				System.Environment.GetEnvironmentVariable("ProgramW6432"),
				System.Environment.GetEnvironmentVariable("ProgramFiles")
			};

			//System.Windows.Forms.MessageBox.Show(programFilesFolders[0] + "\n" + programFilesFolders[1]);

			string[] paths = new string[] 
			{ 
				"v7.0A\\Bin\\SqlMetal.exe",
				"v7.0A\\Bin\\x64\\SqlMetal.exe",
				"v7.0\\Bin\\SqlMetal.exe",
				"v7.0\\Bin\\x64\\SqlMetal.exe",
				"v6.0A\\Bin\\SqlMetal.exe",
				"v6.0A\\Bin\\x64\\SqlMetal.exe",
				"v6.0\\Bin\\SqlMetal.exe",
				"v6.0\\Bin\\x64\\SqlMetal.exe"
			};

			foreach (var programFilesFolder in programFilesFolders)
			{
				if (string.IsNullOrEmpty(programFilesFolder))
					continue;

				string programFiles = Path.Combine(programFilesFolder, "Microsoft SDKs\\Windows\\");

				foreach (var path in paths)
				{
					sqlMetalPath = Path.Combine(programFiles, path);
					if (File.Exists(sqlMetalPath))
						return true;
				}
			}

			WriteLine("ERROR: SqlMetal.exe not found. Please install Windows SDK.");
			statusBarText("SqlMetal.exe not found. Please install Windows SDK.");
			return false;
		}
	}

	public enum WorkerItemMode
	{
		None,
		AllExceptListed,
		OnlyListed
	}
}
